前几天讲解过 Socket
的相关知识,但是在查找资料后看到 IM
zhong还有其他的相关协议。今天小编就讲解其他的协议:WebSocket
。
之所以研究 WebSocket
原因是相比较与 Socket
,WebSocket
可以是 server
端向 client
进行信息发送,不需要 client
为获取信息而采用长期轮询方式来获取信息。
WebSocket
简介
起源
WebSocket
是一种在单个TCP连接上进行全双工通讯的协议。通信协议于 2011 年被 IETF
定为标准 RFC 6455
,并由 RFC7936
补充规范。
目前浏览器对服务器的访问发送 HTTP
请求,然后由服务器返回最新的数据给客户端的浏览器。但是网站如果要实现推送技术,就需要采用轮询。但是这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP
请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
而比较新的技术去做轮询的效果是 Comet
。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在 Comet
中,普遍采用的长链接,也会消耗服务器资源。而 WebSocket
协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
Websocket
使用和 HTTP
相同的 TCP
端口,可以绕过大多数防火墙的限制。默认情况下,Websocket
协议使用 80 端口;运行在 TLS
之上时,默认使用 443 端口。
轮询和WebSocket
查询
下面展示初始轮询和优化版长轮询方式以及
WebSocket
信息传递结构图
- 初始轮询结构图
【图片来源于网络】
由上图可以看出:
在初始轮询过程中每次进行数据访问都要执行数据访问申请。但是这种方式不适合做轮询,因为这样需要
client
每个一段时间就会向server
发起请求。这样就会每次请求都会包含HTTP
的Header
hui增加流量,也会消耗GPU
。
- 优化长轮询结构图
【图片来源于网络】
由上图可以看出:
此方法是对轮询的改进版本,
client
发送HTTP
给server
后,如果没有得到新的消息,就会处于等地状态。当有消息才会返回信息给client
。这样在一定的程度上可以减轻宽带的压力和GPU
的消耗。
但是这样有个弊端:如果在聊天过程中server
在不断更新,这样client
必须要等到下一次请求才能够发送消息。client
显示实时数据最快的时间为2×RTT(往返时间),而且如果在网络拥塞的情况下,这个时间用户是不能接受的。
WebSocket
轮询方式
【图片来源于网络】
由上图可以看出:
Websocket
和 轮询方式Polling
的区别,从图中可以看到Polling
里client
发送了好多Request
,而下图,只有一个Upgrade
,非常简洁高效。
为解决协议的头部没有 HTTP
的 Header
和 能支持客户端和服务器端的双向通信,Websocket
就诞生了!
WebSocket
协议原理
WebSocket
的实现分为:握手、数据发送/读取 和 关闭连接。
Websocket
是第七层上应用层的一个应用层协议,它必须依赖 HTTP
协议进行一次握手 ,握手成功后,数据就直接从 TCP
通道传输,与 HTTP
无关了。
Websocket
的数据传输是 frame
形式传输的,比如会将一条消息分为几个 frame
,按照先后顺序传输出去。这样做会有几个好处:
1) 大数据的传输可以分片传输,不用考虑到数据大小导致的长度标志位不足够的情况。
2) 和http
的chunk
一样,可以边生成数据边传递消息,即提高传输效率。
- 握手
握手需要从请求头理解。
WebSocket
首先发起一个 HTTP
请求,在请求头加上 Upgrade
字段,该字段用于改变 HTTP
协议版本或者是换用其他协议,这里我们把 Upgrade
的值设为 websocket
,将它升级为 WebSocket
协议。
同时要注意 Sec-WebSocket-Key
字段,它由客户端生成并发给服务端,用于证明服务端接收到的是一个可受信的连接握手,可以帮助服务端排除自身接收到的由非 WebSocket
客户端发起的连接,该值是一串随机经过 base64
编码的字符串。
1 | GET /chat HTTP/1.1 |
我们可以简化请求头,将请求以字符串方式发送出去,当然别忘了最后的两个空行作为包结束:
1 | const char * fmt = "GET %s HTTP/1.1\r\n" |
收到请求后,服务端也会做一次响应:
1 | HTTP/1.1 101 Switching Protocols |
里面重要的是 Sec-WebSocket-Accept
,服务端通过从客户端请求头中读取 Sec-WebSocket-Key
与一串全局唯一的标识字符串(俗称魔串)“258EAFA5-E914-47DA- 95CA-C5AB0DC85B11”
做拼接,生成长度为 160 位的 SHA-1
字符串,然后进行 base64
编码,作为 Sec-WebSocket-Accept
的值回传给客户端。
处理握手
HTTP
响应解析的时候,可以用nodejs
的http-paser
,解析方式也比较简单,就是对头信息的逐字读取再处理,具体处理你可以看一下它的状态机实现。解析完成后你需要对其内容进行解析,看返回是否正确,同时去管理你的握手状态。
- 数据发送/读取
数据的处理就要拿这个帧协议图来说明了如下:
1 | 0 1 2 3 |
1)数据发送:
1 | void ws__wrap_packet(_WS_IN websocket_t *ws, |
2)数据解析:
1 | int ws_recv(websocket_t *ws) { |
- 关闭连接
关闭连接分为两种:
(1)服务端发起关闭;
(2)客户端主动关闭。
服务端跟客户端的处理基本一致,以服务端为例:
服务端发起关闭的时候,xiang客户端发送一个关闭帧,客户端在接收到帧的时候通过解析出帧的opcode来判断是否是关闭帧,然后同样向服务端再发送一个关闭帧作为回应。
1 | if (op_code == OP_CLOSE) { |
参考地址:
WebSocket